package com.jediterm.terminal.model;

import com.jediterm.terminal.StyledTextConsumer;
import com.jediterm.terminal.TextStyle;
import com.jediterm.terminal.util.CharUtils;
import com.jediterm.terminal.util.Pair;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;

/**
 * @author traff
 */
public final class TerminalLine {
    private static final Logger LOG = LoggerFactory.getLogger(com.jediterm.terminal.model.TerminalLine.class);

    private TextEntries myTextEntries = new TextEntries();
    private boolean myWrapped = false;
    private final List<TerminalLineIntervalHighlighting> myCustomHighlightings = new ArrayList<>();
    com.jediterm.terminal.model.TerminalLine myTypeAheadLine;

    public TerminalLine() {
    }

    public TerminalLine(@NotNull TextEntry entry) {
        myTextEntries.add(entry);
    }

    public static com.jediterm.terminal.model.TerminalLine createEmpty() {
        return new com.jediterm.terminal.model.TerminalLine();
    }

    public @NotNull String getText() {
        StringBuilder result = new StringBuilder(myTextEntries.myLength);
        for (com.jediterm.terminal.model.TerminalLine.TextEntry textEntry : myTextEntries) {
            // NUL can only be at the end
            if (textEntry.getText().isNul()) {
                break;
            }
            result.append(textEntry.getText());
        }
        return result.toString();
    }

    public @NotNull com.jediterm.terminal.model.TerminalLine copy() {
        com.jediterm.terminal.model.TerminalLine result = new com.jediterm.terminal.model.TerminalLine();
        for (TextEntry entry : myTextEntries) {
            result.myTextEntries.add(entry);
        }
        result.myWrapped = myWrapped;
        return result;
    }

    public char charAt(int x) {
        com.jediterm.terminal.model.TerminalLine typeAheadLine = myTypeAheadLine;
        if (typeAheadLine != null) {
            return typeAheadLine.charAt(x);
        }
        String text = getText();
        return x < text.length() ? text.charAt(x) : CharUtils.EMPTY_CHAR;
    }

    public boolean isWrapped() {
        return myWrapped;
    }

    public void setWrapped(boolean wrapped) {
        myWrapped = wrapped;
    }

    public synchronized void clear(@NotNull TextEntry filler) {
        myTextEntries.clear();
        myTextEntries.add(filler);
        setWrapped(false);
    }

    public void writeString(int x, @NotNull CharBuffer str, @NotNull TextStyle style) {
        writeCharacters(x, style, str);
    }

    public void insertString(int x, @NotNull CharBuffer str, @NotNull TextStyle style) {
        insertCharacters(x, style, str);
    }

    private synchronized void writeCharacters(int x, @NotNull TextStyle style, @NotNull CharBuffer characters) {
        int len = myTextEntries.length();

        if (x >= len) {
            // fill the gap
            if (x - len > 0) {
                myTextEntries.add(new TextEntry(TextStyle.EMPTY, new CharBuffer(CharUtils.NUL_CHAR, x - len)));
            }
            myTextEntries.add(new TextEntry(style, characters));
        } else {
            len = Math.max(len, x + characters.length());
            myTextEntries = merge(x, characters, style, myTextEntries, len);
        }
    }

    private synchronized void insertCharacters(int x, @NotNull TextStyle style, @NotNull CharBuffer characters) {
        int length = myTextEntries.length();
        if (x > length) {
            writeCharacters(x, style, characters);
            return;
        }

        Pair<char[], TextStyle[]> pair = toBuf(myTextEntries, length + characters.length());

        for (int i = length - 1; i >= x; i--) {
            pair.first[i + characters.length()] = pair.first[i];
            pair.second[i + characters.length()] = pair.second[i];
        }
        for (int i = 0; i < characters.length(); i++) {
            pair.first[i + x] = characters.charAt(i);
            pair.second[i + x] = style;
        }
        myTextEntries = collectFromBuffer(pair.first, pair.second);
    }

    private static TextEntries merge(int x, @NotNull CharBuffer str, @NotNull TextStyle style, @NotNull TextEntries entries, int lineLength) {
        Pair<char[], TextStyle[]> pair = toBuf(entries, lineLength);

        for (int i = 0; i < str.length(); i++) {
            pair.first[i + x] = str.charAt(i);
            pair.second[i + x] = style;
        }

        return collectFromBuffer(pair.first, pair.second);
    }

    private static Pair<char[], TextStyle[]> toBuf(TextEntries entries, int lineLength) {
        Pair<char[], TextStyle[]> pair = Pair.create(new char[lineLength], new TextStyle[lineLength]);


        int p = 0;
        for (TextEntry entry : entries) {
            for (int i = 0; i < entry.getLength(); i++) {
                pair.first[p + i] = entry.getText().charAt(i);
                pair.second[p + i] = entry.getStyle();
            }
            p += entry.getLength();
        }
        return pair;
    }

    private static TextEntries collectFromBuffer(char[] buf, @NotNull TextStyle[] styles) {
        TextEntries result = new TextEntries();

        TextStyle curStyle = styles[0];
        int start = 0;

        for (int i = 1; i < buf.length; i++) {
            if (styles[i] != curStyle) {
                result.add(new TextEntry(curStyle, new CharBuffer(buf, start, i - start)));
                curStyle = styles[i];
                start = i;
            }
        }

        result.add(new TextEntry(curStyle, new CharBuffer(buf, start, buf.length - start)));

        return result;
    }

    public synchronized void deleteCharacters(int x) {
        deleteCharacters(x, TextStyle.EMPTY);
    }

    public synchronized void deleteCharacters(int x, @NotNull TextStyle style) {
        deleteCharacters(x, myTextEntries.length() - x, style);
        // delete to the end of line : line is no more wrapped
        setWrapped(false);
    }

    public synchronized void deleteCharacters(int x, int count, @NotNull TextStyle style) {
        int p = 0;
        TextEntries newEntries = new TextEntries();

        int remaining = count;

        for (TextEntry entry : myTextEntries) {
            if (remaining == 0) {
                newEntries.add(entry);
                continue;
            }
            int len = entry.getLength();
            if (p + len <= x) {
                p += len;
                newEntries.add(entry);
                continue;
            }
            int dx = x - p; //>=0
            if (dx > 0) {
                //part of entry before x
                newEntries.add(new TextEntry(entry.getStyle(), entry.getText().subBuffer(0, dx)));
                p = x;
            }
            if (dx + remaining < len) {
                //part that left after deleting count
                newEntries.add(new TextEntry(entry.getStyle(), entry.getText().subBuffer(dx + remaining, len - (dx + remaining))));
                remaining = 0;
            } else {
                remaining -= (len - dx);
                p = x;
            }
        }
        if (count > 0 && style != TextStyle.EMPTY) { // apply style to the end of the line
            newEntries.add(new TextEntry(style, new CharBuffer(CharUtils.NUL_CHAR, count)));
        }

        myTextEntries = newEntries;
    }

    public synchronized void insertBlankCharacters(int x, int count, int maxLen, @NotNull TextStyle style) {
        int len = myTextEntries.length();
        len = Math.min(len + count, maxLen);

        char[] buf = new char[len];
        TextStyle[] styles = new TextStyle[len];

        int p = 0;
        for (TextEntry entry : myTextEntries) {
            for (int i = 0; i < entry.getLength() && p < len; i++) {
                if (p == x) {
                    for (int j = 0; j < count && p < len; j++) {
                        buf[p] = CharUtils.EMPTY_CHAR;
                        styles[p] = style;
                        p++;
                    }
                }
                if (p < len) {
                    buf[p] = entry.getText().charAt(i);
                    styles[p] = entry.getStyle();
                    p++;
                }
            }
            if (p >= len) {
                break;
            }
        }

        // if not inserted yet (ie. x > len)
        for (; p < x && p < len; p++) {
            buf[p] = CharUtils.EMPTY_CHAR;
            styles[p] = TextStyle.EMPTY;
            p++;
        }
        for (; p < x + count && p < len; p++) {
            buf[p] = CharUtils.EMPTY_CHAR;
            styles[p] = style;
            p++;
        }

        myTextEntries = collectFromBuffer(buf, styles);
    }

    public synchronized void clearArea(int leftX, int rightX, @NotNull TextStyle style) {
        if (rightX == -1) {
            rightX = Math.max(myTextEntries.length(), leftX);
        }
        writeCharacters(leftX, style, new CharBuffer(
                rightX >= myTextEntries.length() ? CharUtils.NUL_CHAR : CharUtils.EMPTY_CHAR,
                rightX - leftX));
    }

    @Nullable
    public synchronized TextStyle getStyleAt(int x) {
        int i = 0;

        for (TextEntry te : myTextEntries) {
            if (x >= i && x < i + te.getLength()) {
                return te.getStyle();
            }
            i += te.getLength();
        }

        return null;
    }

    public synchronized void process(int y, StyledTextConsumer consumer, int startRow) {
        int x = 0;
        int nulIndex = -1;
        TerminalLineIntervalHighlighting highlighting = myCustomHighlightings.stream().findFirst().orElse(null);
        com.jediterm.terminal.model.TerminalLine typeAheadLine = myTypeAheadLine;
        TextEntries textEntries = typeAheadLine != null ? typeAheadLine.myTextEntries : myTextEntries;
        for (TextEntry te : textEntries) {
            if (te.getText().isNul()) {
                if (nulIndex < 0) {
                    nulIndex = x;
                }
                consumer.consumeNul(x, y, nulIndex, te.getStyle(), te.getText(), startRow);
            } else {
                if (highlighting != null && te.getLength() > 0 && highlighting.intersectsWith(x, x + te.getLength())) {
                    processIntersection(x, y, te, consumer, startRow, highlighting);
                } else {
                    consumer.consume(x, y, te.getStyle(), te.getText(), startRow);
                }
            }
            x += te.getLength();
        }
        consumer.consumeQueue(x, y, nulIndex < 0 ? x : nulIndex, startRow);
    }

    private void processIntersection(int startTextOffset, int y, @NotNull TextEntry te, @NotNull StyledTextConsumer consumer,
                                     int startRow, @NotNull TerminalLineIntervalHighlighting highlighting) {
        CharBuffer text = te.getText();
        int endTextOffset = startTextOffset + text.length();
        int[] offsets = new int[]{startTextOffset, endTextOffset, highlighting.getStartOffset(), highlighting.getEndOffset()};
        Arrays.sort(offsets);
        int startTextOffsetInd = Arrays.binarySearch(offsets, startTextOffset);
        int endTextOffsetInd = Arrays.binarySearch(offsets, endTextOffset);
        if (startTextOffsetInd < 0 || endTextOffsetInd < 0) {
            LOG.error("Cannot find " + Arrays.toString(new int[]{startTextOffset, endTextOffset})
                    + " in " + Arrays.toString(offsets) + ": " + Arrays.toString(new int[]{startTextOffsetInd, endTextOffsetInd}));
            consumer.consume(startTextOffset, y, te.getStyle(), text, startRow);
            return;
        }
        for (int i = startTextOffsetInd; i < endTextOffsetInd; i++) {
            int length = offsets[i + 1] - offsets[i];
            if (length == 0) continue;
            CharBuffer subText = new SubCharBuffer(text, offsets[i] - startTextOffset, length);
            if (highlighting.intersectsWith(offsets[i], offsets[i + 1])) {
                consumer.consume(offsets[i], y, highlighting.mergeWith(te.getStyle()), subText, startRow);
            } else {
                consumer.consume(offsets[i], y, te.getStyle(), subText, startRow);
            }
        }
    }

    public synchronized boolean isNul() {
        for (TextEntry e : myTextEntries) {
            if (!e.isNul()) {
                return false;
            }
        }

        return true;
    }

    void forEachEntry(@NotNull Consumer<TextEntry> action) {
        myTextEntries.forEach(action);
    }

    @TestOnly
    public List<TextEntry> getEntries() {
        return Collections.unmodifiableList(myTextEntries.entries());
    }

    void appendEntry(@NotNull TextEntry entry) {
        myTextEntries.add(entry);
    }

    @SuppressWarnings("unused") // used by IntelliJ
    public synchronized @NotNull TerminalLineIntervalHighlighting addCustomHighlighting(int startOffset, int length, @NotNull TextStyle textStyle) {
        TerminalLineIntervalHighlighting highlighting = new TerminalLineIntervalHighlighting(this, startOffset, length, textStyle) {
            @Override
            protected void doDispose() {
                synchronized (com.jediterm.terminal.model.TerminalLine.this) {
                    myCustomHighlightings.remove(this);
                }
            }
        };
        myCustomHighlightings.add(highlighting);
        return highlighting;
    }

    @Override
    public String toString() {
        return myTextEntries.length() + " chars, " +
                (myWrapped ? "wrapped, " : "") +
                myTextEntries.myTextEntries.size() + " entries: " +
                myTextEntries.myTextEntries.stream()
                        .map(entry -> entry.getText().toString())
                        .collect(Collectors.joining("|"));
    }

    public static class TextEntry {
        private final TextStyle myStyle;
        private final CharBuffer myText;

        public TextEntry(@NotNull TextStyle style, @NotNull CharBuffer text) {
            myStyle = style;
            myText = text.clone();
        }

        public TextStyle getStyle() {
            return myStyle;
        }

        public CharBuffer getText() {
            return myText;
        }

        public int getLength() {
            return myText.length();
        }

        public boolean isNul() {
            return myText.isNul();
        }

        @Override
        public String toString() {
            return myText.length() + " chars, style: " + myStyle + ", text: " + myText;
        }
    }

    private static class TextEntries implements Iterable<TextEntry> {
        private final List<TextEntry> myTextEntries = new ArrayList<>();

        private int myLength = 0;

        public void add(TextEntry entry) {
            // NUL can only be at the end of the line
            if (!entry.getText().isNul()) {
                for (TextEntry t : myTextEntries) {
                    if (t.getText().isNul()) {
                        t.getText().unNullify();
                    }
                }
            }
            myTextEntries.add(entry);
            myLength += entry.getLength();
        }

        private List<TextEntry> entries() {
            return myTextEntries;
        }

        @NotNull
        public Iterator<TextEntry> iterator() {
            return myTextEntries.iterator();
        }

        public int length() {
            return myLength;
        }

        public void clear() {
            myTextEntries.clear();
            myLength = 0;
        }
    }
}
